第5章 基本引用类型(2)

原始值包装类型

JS提供了 3 种特殊的引用类型:Boolean、Number 和 String。这些类型既具有引用类型的特点,又具有与原始类型对应的行为。

let s1 = "some text"; // s1 是一个包含字符串的变量,它是一个原始值
let s2 = s1.substring(2); // 在s1上调用了substring()方法并将结果赋值给s2

原始值本身不是对象,因此逻辑上不应该有方法。但是实际是后台创建了一个与s1相同内容的String类型的实例,进而在这个实例上调用了substring()方法,调用结束后再将结果赋值给s2并销毁了实例

let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;

这种行为可以让原始值拥有对象的行为,Boolean和Number也是同样的步骤

自动创建的原始值包装对象只存在于访问它的那行代码执行期间

let s1 = "some text";
s1.color = "red"; // 运行完这一句代码,自动创建的实例就被销毁了
console.log(s1.color);  // undefined,此时又创建了一个新的实例,但是该实例没有color属性

在原始值包装类型的实例上调用 typeof 会返回"object",所有原始值包装对象都会转换为布尔值 true。Object 构造函数能够根据传入值的类型返回相应原始值包装类型的实例:

let value = "25";
let number = Number(value); // 转型函数
console.log(typeof number); // "number"
let obj = new Number(value); // 构造函数
console.log(typeof obj); // "object"

let obj = new Object("some text");
console.log(obj instanceof String);  // true

Boolean:知道即可,不建议直接实例化Boolean对象,容易出现误会

let falseObject = new Boolean(false); // 生成 Boolean 实例
let result = falseObject && true;
console.log(result); // true,因为所有对象转换成布尔值都为true

let falseValue = false;
result = falseValue && true;
console.log(result); // false

console.log(typeof falseObject);             // object
console.log(typeof falseValue);              // boolean
console.log(falseObject instanceof Boolean); // true
console.log(falseValue instanceof Boolean);  // false

Number:对应数值的引用类型,也不建议直接实例化Number对象

与 Boolean 类型一样,Number 类型重写了 valueOf()、toLocaleString()和 toString()方法。valueOf()方法返回 Number 对象表示的原始数值,另外两个方法返回数值字符串。toString() 方法可选地接收一个表示基数的参数,并返回相应基数形式的数值字符串:

let num = 10;
console.log(num.toString());   // "10"
console.log(num.toString(2));  // "1010"
console.log(num.toString(8));  // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"

除了继承的方法,Number 类型还提供了几个用于将数值格式化为字符串的方法

let num = 10;
// toFixed()方法返回包含指定小数点位数的数值字符串
console.log(num.toFixed(2)); // "10.00"
num = 10.005;
console.log(num.toFixed(2)); // "10.01",支持四舍五入
// toExponential()方法返回以科学记数法表示的数值字符串
// 所接收的参数表示小数的位数
num = 10;
console.log(num.toExponential(1));  // "1.0e+1"
// toPrecision()方法会根据情况返回最合理的输出结果
// 接收一个参数,表示结果中数字的总位数(不包含指数)
let num = 99;
console.log(num.toPrecision(1)); // "1e+2",因为99不能只用1位数字来精确表示,所以这个方法就将它舍入为100,这样就可以只用1位数字表示
console.log(num.toPrecision(2)); // "99"
console.log(num.toPrecision(3)); // "99.0"
// Number.isInteger()方法判断一个数值是否为整数
console.log(Number.isInteger(1));    // true
console.log(Number.isInteger(1.00)); // true
console.log(Number.isInteger(1.01)); // false
// Number.isSafeInteger()方法判断是否超出了正常范围
console.log(Number.isSafeInteger(-1 * (2 ** 53))); // false
console.log(Number.isSafeInteger(-1 * (2 ** 53) + 1)); // true
console.log(Number.isSafeInteger(2 ** 53)); // false
console.log(Number.isSafeInteger((2 ** 53) - 1)); // true

String:用的最多

使用 String 构造函数并传入一个字符串:

let stringObj = new String("hello world");

String 类型提供了很多方法来解析和操作字符串

let message = "abcde";
console.log(message.length); // 5,获取字符串长度
console.log(message.charAt(2)); // "c",获取指定索引位置的字符
console.log(message.charCodeAt(2)); // 99,查看指定码元的字符编码
console.log(String.fromCharCode(0x61, 0x62, 0x63, 0x64, 0x65)); // "abcde",根据给定的 UTF-16 码元创建字符串中的字符
console.log(String.fromCharCode(97, 98, 99, 100, 101));
// "abcde",传入的数值也可以是十进制数字

常用的字符串操作方法

// 拼接字符串
let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result);      // "hello world"
console.log(stringValue); // "hello"
// 相比于 concat() 方法,使用加号操作符"+"更为常见
// 截取字符串
let stringValue = "hello world";
console.log(stringValue.slice(3, 7)); // "lo w",第一个参数表示开始位置,第二个参数表示结束位置
console.log(stringValue.substring(3,7)); // "lo w",第一个参数表示开始位置,第二个参数表示结束位置
console.log(stringValue.substr(3, 7)); // "lo worl",第一个参数表示开始位置,第二个参数表示字符串长度

// 省略第二个参数则截取到字符串末尾
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"

// 如果参数为负值
// slice()方法将所有负值都与字符串长度相加,再计算
console.log(stringValue.slice(-3)); // "rld"
console.log(stringValue.slice(3, -4)); // "lo w"

// substring()将所有负值都转换为0
console.log(stringValue.substring(-3)); // "hello world"
console.log(stringValue.substring(3, -4)); // "hel",等价于substring(0, 3)
// 因为该方法会将较小的参数作为起点,将较大的参数作为终点

// substr()将第一个负值与字符串长度相加,第二个负值转换为0
console.log(stringValue.substr(-3)); // "rld"
console.log(stringValue.substr(3, -4)); // "",因为第二个参数为0
// 字符串位置
let stringValue = "hello world";
console.log(stringValue.indexOf("o"));     // 4,从字符串开头开始查找子字符串
console.log(stringValue.lastIndexOf("o")); // 7,从字符串末尾开始查找子字符串

// 第二个参数用来指定搜索的起始位置
console.log(stringValue.indexOf("o", 6));     // 7
console.log(stringValue.lastIndexOf("o", 6)); // 4,因为从位置6开始反向搜索,搜索到的是hello里的字母o
// 字符串包含
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.includes("bar"));   // true
// 第二个参数用来指定搜索的起始位置
console.log(message.startsWith("foo", 1));  // false
console.log(message.includes("bar", 4));    // false

console.log(message.endsWith("baz"));   // true
// 第二个参数表示应该当作字符串末尾的位置
console.log(message.endsWith("bar"));     // false
console.log(message.endsWith("bar", 6));  // true
// trim() 方法,删除字符串的前后空格
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
// trimLeft() 和 trimRight() 方法分别用于从字符串开始和末尾清理空格符
// repeat() 方法,将字符串复制多少次
let stringValue = "na ";
console.log(stringValue.repeat(16) + "batman");
// na na na na na na na na na na na na na na na na batman
// padStart() 和 padEnd() 方法,在相应一边填充字符,直至满足长度条件
let stringValue = "foo";
console.log(stringValue.padStart(6));       // "   foo"
console.log(stringValue.padStart(9, "."));  // "......foo"
console.log(stringValue.padEnd(6));         // "foo   "
console.log(stringValue.padEnd(9, "."));    // "foo......"
// 可选的第二个参数并不限于一个字符
console.log(stringValue.padStart(8, "bar")); // "barbafoo"
console.log(stringValue.padStart(2)); // "foo"
console.log(stringValue.padEnd(8, "bar")); // "foobarba"
console.log(stringValue.padEnd(2)); // "foo"

字符串迭代器与解构

字符串的原型上暴露了一个@@iterator 方法,可以用来迭代字符串的每个字符

let message = "abc";
let stringIterator = message[Symbol.iterator]();
console.log(stringIterator.next()); // {value: "a", done: false} 
console.log(stringIterator.next()); // {value: "b", done: false} 
console.log(stringIterator.next()); // {value: "c", done: false} 
console.log(stringIterator.next()); // {value: undefined, done: true}

也可以通过 for-of 访问迭代器按序访问每个字符

for (const c of "abcde") {
    console.log(c);
}
// a
// b
// c
// d
// e

可以通过解构操作符来对字符串解构

let message = "abcde";
console.log([...message]); // ["a", "b", "c", "d", "e"]
// 大小写转换
let stringValue = "hello world";
console.log(stringValue.toLocaleUpperCase()); // "HELLO WORLD" 
console.log(stringValue.toUpperCase()); // "HELLO WORLD" 
console.log(stringValue.toLocaleLowerCase()); // "hello world"
console.log(stringValue.toLowerCase()); // "hello world"
// match() 方法,对字符串进行模式匹配
// 这个方法本质上跟 RegExp 对象的 exec() 方法相同
// match() 方法接收一个参数,可以是一个正则表达式字符串,也可以是一个 RegExp 对象
let text = "cat, bat, sat, fat";
let pattern = /.at/;
// 等价于pattern.exec(text)
let matches = text.match(pattern);
console.log(matches.index); // 0
console.log(matches[0]); // "cat" 
console.log(pattern.lastIndex); // 0
// match() 方法的返回值与 RegExp 对象的 exec() 方法返回的结果一致

// search() 方法,参数与 match() 方法一样,返回第一个匹配位置的索引值,没找到则返回-1
let pos = text.search(/at/);
console.log(pos);  // 1
// replace() 方法,查找并替换字符串中的部分文字
// 第一个参数可以是一个 RegExp 对象或一个字符串
// 第二个参数可以是一个字符串或一个函数
// 如果第一个参数是字符串,那么只会替换第一个子字符串
// 如果想替换所有子字符串,则第一个参数必须为正则表达式并且带全局标记
let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"

result = text.replace(/at/g, "ond");
console.log(result);  // "cond, bond, sond, fond"

// 第二个参数是字符串的情况下,可以通过特殊字符插入正则表达式操作的值
// 具体参照书上原文
result = text.replace(/(.at)/g, "word ($1)");
console.log(result); 
// word (cat), word (bat), word (sat), word (fat)
// 每个以"at"结尾的词都会被替换成"word"后跟一对小括号,其中包含捕获组匹配的内容$1

// 第二个参数也可以是一个函数,函数会收到3个参数:模式匹配字符串、匹配项的开始位置、原始字符串
function htmlEscape(text) {
    return text.replace(/[<>"&]/g, function(match, pos, originalText) {    
    switch(match) {
      case "<":
        return "&lt;";
      case ">":
        return "&gt;";
      case "&":
        return "&amp;";
      case "\"":
        return "&quot;";
    }
  }); 
}
console.log(htmlEscape("<p class=\"greeting\">Hello world!</p>"));
// &lt;p class=&quot;greeting&quot;&gt;Hello world!&lt;/p&gt;
// split() 方法,根据传入的分隔符将字符串拆分成数组
// 作为分隔符的参数可以是字符串,也可以是 RegExp 对象
// 还可以传入第二个参数,即数组大小,确保返回的数组不会超过指定大小
let colorText = "red,blue,green,yellow";
let colors1 = colorText.split(","); // ["red", "blue", "green", "yellow"]
let colors2 = colorText.split(",", 2); // ["red", "blue"]
let colors3 = colorText.split(/[^,]+/); // ["", ",", ",", ",", ""]
// localeCompare() 方法,比较两个字符串
// 如果按照字母表顺序,字符串应该排在字符串参数前面,则返回负数(通常为-1)
// 如果字符串与字符串参数相等,则返回0
// 如果按照字母表顺序,字符串应该排在字符串参数后面,则返回正数(通常为1)
let stringValue = "yellow";
console.log(stringValue.localeCompare("brick"));  // 1
console.log(stringValue.localeCompare("yellow")); // 0
console.log(stringValue.localeCompare("zoo"));    // -1

单例内置对象

Global对象,在全局作用域中定义的变量和函数都会变成 Global 对象的属性,前面介绍的函数,包括 isNaN()、isFinite()、parseInt()和 parseFloat(),实际上都是 Global 对象的方法。除了这些,Global 对象上还有另外一些方法:

// encodeURI() 和 encodeURIComponent() 用于对URI进行编码
// encodeURI()不会编码属于 URL 组件的特殊字符,比如冒号、斜杠、问号、井号
// encodeURIComponent()会编码它发现的所有非标准字符
let uri = "http://www.wrox.com/illegal value.js#start";
console.log(encodeURI(uri));
// "http://www.wrox.com/illegal%20value.js#start"
console.log(encodeURIComponent(uri));
// "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start"
// decodeURI() 和 decodeURIComponent() 用于对编码过的URI解码
// decodeURI() 相对于 encodeURI()
// decodeURIComponent() 相对于 encodeURIComponent()
let uri = "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start";
console.log(decodeURI(uri));
// http%3A%2F%2Fwww.wrox.com%2Fillegal value.js%23start
console.log(decodeURIComponent(uri));
// http://www.wrox.com/illegal value.js#start
// eval() 方法,这个方法就是一个完整的 ECMAScript 解释器
// 它接收一个参数,即一个要执行的 ECMAScript(JavaScript) 字符串
// 了解即可,不要用!不要用!不要用!
eval("console.log('hi')");
// 上面这行代码的功能与下面这行等价:
console.log("hi");
// 定义在上下文中的变量可以在 eval() 方法中被引用
let msg1 = "hello world";
eval("console.log(msg1)");  // "hello world"
// 同样的,定义在 eval() 内部的函数或变量,可以在外部代码中引用
eval("function sayHi() { console.log('hi'); }");
sayHi();
// 通过 eval() 定义的变量和函数都不会被提升
eval("let msg2 = 'hello world';");
console.log(msg2);  // Reference Error: msg2 is not defined

window对象

浏览器将 window 对象实现为 Global 对象的代理,因此所有全局作用域中声明的变量和函数都变成了 window 的属性:

var color = "red";
function sayColor() {
  console.log(window.color);
}
window.sayColor(); // "red"

Math对象,用于保存数学公式、信息和计算。Math对象有一些属性,用于保存数学中的一些特殊值,这里就提一个 Math.PI,用于记录π的值。其他的可以现用现查,不过一般也用不到。再就是几个常用的方法:

// min() 和 max() 方法,用于确定一组数值中的最小值和最大值
let max = Math.max(3, 54, 32, 16);
console.log(max);  // 54
let min = Math.min(3, 54, 32, 16);
console.log(min);  // 3

// ceil()、floor()、round() 和 fround(),用于小数值的舍入
// Math.ceil() 方法始终向上舍入为最接近的整数
console.log(Math.ceil(25.9)); // 26
console.log(Math.ceil(25.5)); // 26
console.log(Math.ceil(25.1)); // 26
// Math.floor() 方法始终向下舍入为最接近的整数
console.log(Math.floor(25.9));  // 25
console.log(Math.floor(25.5));  // 25
console.log(Math.floor(25.1));  // 25
// Math.round() 方法执行四舍五入
console.log(Math.round(25.9));  // 26
console.log(Math.round(25.5));  // 26
console.log(Math.round(25.1));  // 25
// Math.fround() 方法返回数值最接近的单精度(32 位)浮点值表示
console.log(Math.fround(0.4));  // 0.4000000059604645
console.log(Math.fround(0.5));  // 0.5
console.log(Math.fround(25.9)); // 25.899999618530273

// random() 方法,生成一个0-1范围内的随机数,包含0但不包含1
// 生成1-10范围内的随机数
let num1 = Math.floor(Math.random() * 10 + 1);
// 如果想生成2-10范围内的值,则可以如下实现
let num2 = Math.floor(Math.random() * 9 + 2);

Math对象还有很多其他方法,具体用法可以参考书上原文